﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;

namespace RPGMapRevealer
{
    /// <summary>
    /// Represents a single tile on the Map
    /// </summary>
    public class Tile
    {
        /// <summary>
        /// The X position of the tile
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "X")]
        public int X { get; set; }
        /// <summary>
        /// The Y position of the tile
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Y")]
        public int Y { get; set; }

        /// <summary>
        /// The Map this tile belongs to
        /// </summary>
        public MapInfo Map { get; set; }

        /// <summary>
        /// Returns true if the specified tile is empty
        /// </summary>
        public bool IsEmpty
        {
            get
            {
                return HasSecret && IsRevealed ? IsEmptySecret : IsEmptyPublic;
            }
        }

        /// <summary>
        /// Returns true if the specified tile is always empty, even when secrets are revealed
        /// </summary>
        public bool IsAlwaysEmpty
        {
            get
            {
                return IsEmptyPublic && IsEmptySecret;
            }
        }

        /// <summary>
        /// Returns true if the tile is empty when secrets are not revealed (Ie a secret passageway shows up as a wall section)
        /// </summary>
        public bool IsEmptyPublic
        { get; set; }
        /// <summary>
        /// Returns true if the tile is empty when secrets are revealed (Ie the space between rooms is always empty)
        /// </summary>
        public bool IsEmptySecret
        { get; set; }

        /// <summary>
        /// Returns the current name of the image depending on its secret and revealed properties
        /// </summary>
        public String ImageName
        {
            get
            {
                return HasSecret && IsRevealed && !String.IsNullOrWhiteSpace(ImageNameSecret) ? ImageNameSecret : ImageNamePublic;
            }
        }

        /// <summary>
        /// The name of the image for when this tile is showing publicly known information
        /// </summary>
        public String ImageNamePublic
        { get; set; }
        /// <summary>
        /// The name of the image for when this tile is show secret information
        /// </summary>
        public String ImageNameSecret
        { get; set; }

        /// <summary>
        /// True if this tile has two modes (public/secret), false if it only has one (public)
        /// </summary>
        public bool HasSecret
        { get; set; }

        /// <summary>
        /// True if the tile has been "revealed" exposing any of it's secrets
        /// </summary>
        public bool IsRevealed
        { get; set; }

        /// <summary>
        /// The level of light being cast on this tile
        /// </summary>
        public int LightLevel
        { get; set; }

        /// <summary>
        /// True if light should be supressed
        /// </summary>
        public bool SupressLight
        { get; set; }

        /// <summary>
        /// Returns the lightlevel unless supressed;
        /// </summary>
        public int EffectiveLightLevel
        { get { return SupressLight || (IsEmptyPublic && !IsRevealed) ? 0 : LightLevel; } }

        /// <summary>
        /// The level of light this tile casts on itself and other tiles
        /// </summary>
        /// <remarks>Note you must call MapInfo.recalculateLight to see the updated results</remarks>
        public int LightSource
        { get; set; }

        private IList<Tile> _adjacentTiles = new List<Tile>();
        /// <summary>
        /// A list of tiles adjacent to this one
        /// </summary>
        public IList<Tile> AdjacentTiles
        {
            get
            {
                Contract.Ensures(Contract.Result<IList<Tile>>() != null);
                return _adjacentTiles;
            }
        }
        private IList<Tile> _losadjacentTiles = new List<Tile>();
        public IList<Tile> NoLOSBetweenTiles
        {
            get
            {
                Contract.Ensures(Contract.Result<IList<Tile>>() != null);
                return _losadjacentTiles;
            }
        }

        /// <summary>
        /// X=#, Y=#, Name=Public/Secret
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return "X=" + X + ", Y=" + Y + ", Name=" + ImageNamePublic + (HasSecret ? "/" + ImageNameSecret : "");
        }

        /// <summary>
        /// Searches through a list of tiles for tiles matching certain criteria, results are returned in blocks starting denoting their distance from the initial tile
        /// </summary>
        /// <param name="distance">The maximum distance to search from this tile</param>
        /// <param name="includeVisibleOnly">True to only include tiles which are currently visible</param>
        /// <param name="condition">Some arbitrary condition which you can decide</param>
        /// <returns>A stepped list of tiles, each step being one further distance from the origin</returns>
        public IEnumerable<IList<Tile>> Search(int distance = 60, bool includeVisibleOnly = true, Predicate<Tile> condition = null)
        {
            Contract.Requires(distance >= 0);

            HashSet<Tile> seen = new HashSet<Tile>();
            List<Tile> last = new List<Tile>();
            List<Tile> current = new List<Tile>();
            current.Add(this);
            seen.Add(this);
            yield return current;
            for (int d = 0; d < distance; d++)
            {
                last = current;
                current = new List<Tile>();
                if (last.Count == 0)
                { break; }
                foreach (Tile tile in last)
                {
                    foreach (Tile adjacent in tile.AdjacentTiles)
                    {
                        if (!seen.Contains(adjacent))
                        {
                            if (!includeVisibleOnly || (!adjacent.IsEmpty && !tile.NoLOSBetweenTiles.Contains(adjacent)))
                            {
                                if (condition == null || condition(adjacent))
                                {
                                    seen.Add(adjacent);
                                    current.Add(adjacent);
                                }
                            }
                        }
                    }
                }
                yield return current;
            }
        }

        /// <summary>
        /// Returns the distance between two tiles based on their coordinates using manhattan style (diagonal movement not allowed)
        /// </summary>
        /// <param name="other"></param>
        /// <returns>the distance between two tiles based on their coordinates using manhattan style (diagonal movement not allowed)</returns>
        public int DistanceManhattan(Tile other)
        {
            Contract.Requires(other != null);
            Contract.Ensures(Contract.Result<int>() >= 0);
            return Util.Abs(X - other.X) + Util.Abs(Y - other.Y);
        }

        /// <summary>
        /// Returns the walking distance between two tiles without going outside the specified path
        /// </summary>
        /// <param name="other"></param>
        /// <param name="path">if null will use a default stepped search</param>
        /// <returns>Returns the walking distance between two tiles without going outside the specified path or -1 if the two tiles do not have any path between them</returns>
        public int DistanceWalking(Tile other, IEnumerable<IEnumerable<Tile>> path = null)
        {
            if (other == null) { return -1; }
            if (path == null)
            { return DistanceWalking(other, Search()); }
            int stepCounter = 0;
            foreach (IEnumerable<Tile> step in path)
            {
                if (step != null && step.Contains(other))
                { return stepCounter; }
                stepCounter++;
            }
            return -1;
        }

        public IEnumerable<Tile> FindShortestPath(Tile other)
        {
            if (other == null) { return null; }
            int totaldist = DistanceWalking(other);
            if (totaldist == -1) { return null; }
            List<Tile> pathToFollow = new List<Tile>();
            Tile current = this;
            Tile nextTile = current;
            while (true)
            {
                int lowestWalking = int.MaxValue;
                int lowestManhattan = int.MaxValue;
                foreach (Tile tile in current.AdjacentTiles)
                {
                    if (current.NoLOSBetweenTiles.Contains(tile))
                    { continue; }
                    if (pathToFollow.Contains(tile))
                    { continue; }
                    int distW = tile.DistanceWalking(other);
                    int distM = tile.DistanceManhattan(other);
                    if (distW == -1)
                    { continue; }
                    if (distW <= lowestWalking)
                    {
                        if (distW != lowestWalking || distM < lowestManhattan)
                        {
                            lowestWalking = distW;
                            lowestManhattan = distM;
                            nextTile = tile;
                        }
                    }                    
                }                
                pathToFollow.Add(current);
                if (nextTile == null) { return null; }
                if (current == other) { return pathToFollow; }
                current = nextTile;
                nextTile = null;
            }
        }

        public IEnumerable<Tile> CalculateLineOfSight(IEnumerable<Tile> specificIntermediaries = null)
        {
            if (specificIntermediaries == null)
            { return CalculateLineOfSight(Search().Flatten()); }
            Dictionary<int, Tile> tiles = new Dictionary<int, Tile>();
            List<Tile> allTiles = specificIntermediaries.ToList();
            foreach (Tile tile in allTiles)
            {
                if (tile == null) { continue; }
                tiles.Add((tile.X * 9999) + tile.Y, tile);
            }
            List<Tile> result = new List<Tile>();
            foreach (Tile tile in allTiles)
            {
                if (HasLOS(tile, tiles))
                { result.Add(tile); }
            }
            return result;
        }

        private bool HasLOS(Tile target, IDictionary<int, Tile> tiles)
        {
            int tx = target.X;
            int ty = target.Y;
            int cx = X;
            int cy = Y;
            if (cx == tx && cy == ty)
            { return true; }
            double wiggle = 0.25;
            bool los = false;
            los |= CheckLine(cx, cy, tx, ty, tiles);
            los |= CheckLine(cx - wiggle, cy, tx, ty, tiles);
            los |= CheckLine(cx + wiggle, cy, tx, ty, tiles);
            los |= CheckLine(cx, cy - wiggle, tx, ty, tiles);
            los |= CheckLine(cx, cy + wiggle, tx, ty, tiles);
            return los;
        }    

        private bool CheckLine(double sx, double sy, double fx, double fy, IDictionary<int, Tile> tiles)
        {
            double ManhattanDistance = Math.Abs(sx - fx) + Math.Abs(sy - fy);
            double dx = fx - sx;
            double dy = fy - sy;
            Tile currentTile;
            if (!tiles.TryGetValue((int)((sx * 9999) + sy), out currentTile))
            {return false;}
            for (double i = 0; i < ManhattanDistance; i++)
            {
                double percent = i / ManhattanDistance;
                int x = (int)Math.Round(sx + (dx * percent));
                int y = (int)Math.Round(sy + (dy * percent));
                Tile nextTile;
                if (!tiles.TryGetValue((x*9999) + y, out nextTile) || currentTile.NoLOSBetweenTiles.Contains(nextTile))
                { return false; }
                currentTile = nextTile;
            }
            return true;
        }

        private bool _isPlayerPresent;
        /// <summary>
        /// True if there is a player indicator at this tile
        /// </summary>
        public bool IsPlayerPresent
        {
            get { return _isPlayerPresent; }
            internal set { _isPlayerPresent = value; }
        }
        /// <summary>
        /// The list of players present on this tile
        /// </summary>
        /// <returns></returns>
        public IEnumerable<int> PlayersPresent()
        {
            if (Map == null) { return null; }
            return Map.PlayersAtPosition(this);
        }

        [ContractInvariantMethod]
        private void InvariantMethod()
        {
            Contract.Invariant(_adjacentTiles != null);
        }
    }
}
